Pt97 compliance, Add strict gatekeeper mode for Meshtastic/MeshCore ingress#17
Open
mathisono wants to merge 58 commits into
Open
Pt97 compliance, Add strict gatekeeper mode for Meshtastic/MeshCore ingress#17mathisono wants to merge 58 commits into
mathisono wants to merge 58 commits into
Conversation
Removed reference to APRS interoperability with APRS.
1. Add exponential backoff for APRS backend reconnection (5s base, 5min cap) to avoid hammering unreachable servers every poll cycle. 2. Fix recv() to buffer all messages from a single read instead of returning only the first match. Remaining messages are drained via tick() -> router.queue() on the next cycle, and recv() checks the pending buffer before reading more data from the socket. 3. Separate kiss_rxbuf from rxbuf so KISS unframing and TNC2 line buffering never collide. 4. Send APRS ACK for every inbound message that carries a message-id, so the remote station knows Raven received the packet.
mathisono
pushed a commit
to mathisono/Raven
that referenced
this pull request
Jun 3, 2026
- Add strict gatekeeper mode for Meshtastic/MeshCore ingress (Part 97 compliance) - Add APRS bridge with APRS-IS, KISS TCP, and TCP text backends - APRS hardening: reconnect backoff, buffered recv, inbound ACK
4ac0944 to
6c19ac4
Compare
…stic ucode's module resolver rejects cycles in the import graph. The chain config→router→meshtastic→gatekeeper, with config also importing gatekeeper directly, triggers a circular dependency error. Break the cycle by: - Removing 'import gatekeeper' from both router.uc and meshtastic.uc - Adding router.setGatekeeper() to inject the reference at setup time - Passing gatekeeper via config._gatekeeper for meshtastic.uc - Using optional chaining (gatekeeper?.isEnabled()) so both modules work safely before the reference is injected
6c19ac4 to
b56b527
Compare
added 6 commits
June 2, 2026 19:50
When the APRS channel is selected, the input placeholder shows '@callsign message or #group message ...' so users know the direct-message and group syntax without reading docs.
- aprs.process() was empty, so outbound messages from UI never reached
APRS-IS. Now calls send() for locally-originated messages.
- Regex for parsing message IDs didn't handle APRS 1.1 reply-ack
trailing '}', causing {01} to appear in message text and ACKs
not being sent back (Xastir kept retransmitting).
…eed channel - Inbound messages from callsigns with a DM channel (e.g. kj6dzb-4 og==) now route to that DM channel instead of always going to the group feed. - Group members always route to the main APRS feed channel. - Outbound from DM channels auto-routes to the callsign in the channel name, stripping redundant @callsign prefix if present. - Default feed channel renamed to APRS-IS-Feed (no spaces in namekey). - Feed channel is auto-registered at setup even if not in config. - Channel key extracted once at setup for efficient DM matching.
…nly, wire up gatekeeper ref in meshcore - meshcore.uc: accept gatekeeper reference from config, add early-drop comment for encrypted packets - router.uc: when gatekeeper strict, only bridge text_message outbound to MeshCore and Meshtastic - Remove old r12 package artifacts
- aprs.uc: refactor single backend to named backend registry; each backend gets own socket, rxbuf, reconnect state; backward compat with old aprs.backend (singular) config; per-channel backend binding via channelBackendMap; new exports: getBackendNames(), updateChannelBackend(), sendToGroup(), recv(backendName) - groups.uc: new shared module extracted from aprs.uc; group CRUD (getGroup/putGroup/removeGroup/memberOf/allGroups), canRepeat() rate-limit/dedup, parseJoinArgs() command parser, createGroupChannel() for runtime channel creation - commands.uc: new /join, /leave, /groups, /help slash commands; /join #name creates shared-key channel (Meshtastic+MeshCore+AREDN); /join %name creates AREDN-only channel; /join #name CALL1 CALL2 msg creates APRS group + channel + sends; /join #name backend=NAME CALL1 msg binds to specific backend; callsigns force AREDN-only (Part 97 compliance); /leave removes channel + APRS group; /groups lists groups - router.uc: poll multiple APRS sockets via aprs:backendName tags; default case dispatches to aprs.recv(backendName) - config.uc: wire up groups.setup(); persist backend field in channel overrides - channel.uc: store optional backend property on channel objects - event.uc: send aprs_backends list to UI; update channel-backend bindings on newchannels - ui/ui.js: backend dropdown in Configure Channels (only shown when backends configured); typeChannelBackend() handler; aprsBackends global populated from server - ui/ui.css: column width for Backend header - APRS.md: complete rewrite with all slash commands, chat commands, config reference, backend types, multi-backend examples - docs/JOIN_CROSSPLATFORM_ANALYSIS.md: cross-platform join analysis
f09e096 to
6c03770
Compare
- Fix: callsign regex now requires at least one digit, preventing plain English words (radio, check, hello) from being parsed as callsigns in /join and in-chat group commands - New: /backends slash command lists configured APRS backends - Docs: added /backends to APRS.md and /help output - Docs: added AREDN-Only Channels section explaining Part 97 channel separation and prefix conventions
ucode is not JavaScript — it has no global 'undefined'. Unset properties return null. Using '!== undefined' caused a runtime Reference error crashing /join when putGroup() or setLocalChannel() was called. Fixed in groups.uc putGroup() and channel.uc setLocalChannel().
The og== key is what marks a channel as AREDN-only, not the % prefix. Converting #me to %me confused users. Now /join #me CALL1 msg creates '#me og==' — the name the user typed, with the AREDN-only key.
- /channels world: show 'Requesting...' feedback when bridge found, show 'No bridge available' when none exists (was silent) - /backend: alias to /backends (user tried singular form, got nothing) - Unknown commands: reply with helpful error instead of silent drop - process() reply_public_channels: add missing break before default
Two issues prevented outbound messages from group channels: 1. /join without explicit backend= never registered the channel in channelBackendMap, so process() silently dropped outbound messages. Now always binds to default backend. 2. Plain text in a group channel (e.g. #me) was sent to default_group instead of the channel's own group. Now resolves the group from the channel name first, falling back to default_group.
updateChannelBackend(namekey, null) from /join (no explicit backend) was hitting the 'explicitly cleared' branch and removing the channel from channelBackendMap. Now null means 'bind to default' and only empty string means 'explicitly clear'.
- Channel names keep user's prefix (no more # to % swap) - Callsign detection requires at least one digit - Group channels auto-bound to default backend for outbound - Sidebar strips # and % from display labels - Fix typo ARSDN → AREDN
New slash command to export the current channel's messages as a downloadable file: /export — plain text log (default) /export csv — CSV with timestamp, from, message columns /export text — plain text log Server collects messages, formats them, and sends a /export event. UI triggers a browser file download with the formatted data. Filenames use channel name + timestamp, e.g. TacNet-20260604-220900.txt
ucode arrays don't support arbitrary property assignment — setting _namekey on the command array was silently dropped, causing /export to always show 'No channel selected'.
Xastir and YAAC 'Server Ports' emulate APRS-IS tier-two servers and require the standard login handshake with a valid passcode for write access. Without it, the connection is read-only and all injected traffic is silently dropped. Now all text-based backend types (aprsis, tcp_text, xastir, yaac) send the login handshake on connect. The passcode field is optional — omitting it or setting -1 gives receive-only access. Also updated APRS.md with passcode documentation.
TCP connect to an unreachable mesh IP blocks the entire event loop (default SYN timeout ~75s+). Sets SO_SNDTIMEO before connect to fail fast, then clears it after successful connect. Also added xastir_dzb4 backend (tcp_text) to docs and example config. All text-based backends now send APRS-IS login handshake with passcode.
SO_SNDTIMEO doesn't work for connect timeout in ucode's socket API. Switch to SOCK_NONBLOCK with async connect completion check: - connect() returns immediately with EINPROGRESS - handle() polls SO_ERROR to detect completion - 10s deadline auto-retries with exponential backoff - Prevents unreachable mesh IPs from blocking entire event loop
- Add APRS-IS login handshake for tcp_text/xastir/yaac backends - Fix: add 5s connect timeout to prevent blocking on unreachable backends - Fix: non-blocking connect for APRS backends - Fix: use time() instead of now() in checkPendingConnect (scope issue)
Added new features guides for APRS bridge and Strict Gatekeeper mode.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds an optional Strict Gatekeeper mode for Raven bridge deployments that forward Meshtastic or MeshCore text traffic into AREDN.
The goal is to make the bridge fail closed for common amateur-radio compliance risks when crossing from an unlicensed mesh into an AREDN / amateur-radio network.
What changed
gatekeeper.ucpolicy module.strict_gatekeeperconfiguration support:{ "strict_gatekeeper": { "enabled": true, "gateway_callsign": "W6XYZ", "allowed_callsigns": ["KN6PLV", "KJ6DZB"] } }allowed_callsignswhitelist.For example:
STRICT_GATEKEEPER.mddocumenting configuration, behavior, and operational caveats.Rationale
Meshtastic and MeshCore are useful companion meshes, but AREDN runs in a licensed amateur-radio environment where cleartext operation, station identification, and operator control matter.
The previous behavior allowed bridge traffic to be forwarded too openly. This PR introduces an opt-in mode that:
This preserves existing non-strict behavior by default while providing a safer operating mode for regulated deployments.
Notes and caveats
Strict Gatekeeper mode is not cryptographic identity proof. Meshtastic and MeshCore names are user-controlled, so a node can be renamed to look like a valid callsign. For real deployments, operators should use
allowed_callsigns, and a later hardening step should bind allowed operators to stable Meshtastic node IDs or MeshCore public keys.The callsign validator intentionally matches a simple US amateur callsign form: one or two letters, one numeral, and one to three letters. This will reject international callsigns or unusual/special-event formats that do not contain an extractable US-style token.
Dropped packet logging uses the existing
DEBUG0andDEBUG1channels, so headless OpenWrt deployments should confirm service/logd capture before relying on logs for troubleshooting.Testing
Not yet tested on live RF hardware in this PR.
Code was reviewed for ingress chokepoints:
meshtastic.ucbefore decrypt branches run.Follow-up work
MeshCore direct-message handling currently still performs protocol-level decryption inside
meshcore.ucbefore the router-level strict filter sees the decoded text. The router filter prevents non-text or non-rewritten MeshCore ingress from entering the queue, but the strictest future improvement would be to add a MeshCore decoder-level gate similar to the Meshtastic encrypted-packet hard stop.